from map import *
from entities import *
from actions import *
from los import *
from pathfinding import *
from glyphs import *
from ready_dungeon import *
from dungeons import *
from object_list import olib
from powers import power_names

class Dungeon:
  def __init__(self,dungeon_code,dungeon_level,character_level_on_entry,most_recent_main_dungeon_entry_difficulty,special_levels_already_visited):
    self.special_levels_already_visited = special_levels_already_visited
    if not (dungeon_code in special_levels_already_visited) and dungeon_code<>D_MAIN_DUNGEON:
      self.special_levels_already_visited.append(dungeon_code)
    self.turn = long(0)
    self.glyphs = []
    r = get_ready_dungeon(dungeon_code,dungeon_level,character_level_on_entry,most_recent_main_dungeon_entry_difficulty,special_levels_already_visited)
    self.most_recent_main_dungeon_entry_difficulty = most_recent_main_dungeon_entry_difficulty
    if dungeon_code==D_MAIN_DUNGEON:
      self.most_recent_main_dungeon_entry_difficulty = dungeon_level
    self.l = Levels(r)
    self.entity_positions = {} # key is Pos passed through to_tuple(), value is entity
    self.entity_rooms = {} # key is (level,r_down,r_right), value is [entity] or missing
    self.gr = []  # vector of Group
    party = Group(self,G_PARTY,A_FRIENDLY,[])
    party.m.append(Character(self,self.l.entry_point))
    self.gr.append(party)
    self.l.room_at(self.l.entry_point).ever_visited = 1
    self.levels_visited = [self.l.entry_point.level]
    count = 1
    # set up for LOS
    self.shells = make_shells(MAX_MAP_DIM)
    self.rays = Rays(MAX_MAP_DIM) # create the Rays object
    # set up for pathfinding
    self.room_network = Room_network(self)
    # add keys
    self.room_network_for_keys = Room_network(self,for_keys=1)
    self.add_keys()
    # insert monsters
    for level in r.levels:
      for row in level.rooms:
        for room in row:
          for ready_group in room.groups:
            this_group = Group(self,count,ready_group.attitude,[])
            count = count + 1
            # print "group:"
            for i in range(len(ready_group.monster_types)):
              this_group.add_member(self,Pos(room.level,room.r_down,room.r_right,ready_group.locations[i][0],ready_group.locations[i][1]),ready_group.monster_types[i])
              new_member = this_group.m[len(this_group.m)-1]
              # print "placing ",new_member.name," at ",new_member.pos
              if new_member.type==M_SPIDER:
                # kludge - add webs nearby
                to_web = [new_member.pos]
                for angle in (0,45,90,135,180,225,270,315):
                  to_web = to_web + get_cone(self,new_member.pos,angle,2.8,45)
                for pos in to_web:
                  tile = self.l.tile_at(pos) 
                  if tile.type==T_FLOOR:
                    object_type = None
                    if tile.object is not None:
                      object_type = tile.object.type
                    self.l.room_at(pos).tiles[pos.down][pos.right] = Tile(T_WEB,tile.wall_type,tile.floor_type,object_type=object_type)
            self.gr.append(this_group)
    if dungeon_code==D_TUTORIAL:
      self.tutorial = 1
    else:
      self.tutorial = 0
    # set up for noises
    self.noise_types_to_suppress = [] # Empty this every turn, then add noises not to be multiply played into it when they occur.
    
  def new_turn(self):
    self.turn = self.turn + 1
    redraw = 0
    for pos in d.l.temporary_fires:
      if random.random()<0.1:
        d.l.remove_temporary_fire(pos)
        redraw = 1
        occupant = d.get_occupant(pos)
        display.redraw_tile(d,pos,occupant)
    if redraw:
      display.show_glyphs(d)
      display.blit_map_to_screen()
  
  def get_character(self):
    return self.gr[G_PARTY].m[0]
  
  def get_active_pos(self):
    return self.gr[G_PARTY].m[0].pos
  
  def add_group(self,attitude,enemy_groups):
    self.gr.append(Group(self,self.gr.n_members(),attitude,enemy_groups))
  
  def get_group(self,group_id):
    return self.gr[group_id]

  def get_entity(self,id):
    return self.gr[id.group].m[id.member]

  def get_occupant(self,pos):
    # return the entity or None
    return self.entity_positions.get(pos.to_tuple(),None)

  def clear_glyph_stack(self):
    self.glyphs = []
    
  # as opposed to:
  def clear_action_stack(self):
    self.clear_glyph_stack()
    self.get_character().action_queue = []
  
  def line_of_shot(self,entity,to_pos,exclude,exclude_large=0,exclude_all_but_small=1): 
    # return a [Pos], or None
    # exclude is a [Pos] - will be supplemented by all visible non-small entities in the room
    if not entity.pos.same_room(to_pos):
      return None
    this_pos = to_pos.copy()
    this_room = self.l.room_at(this_pos)
    if exclude_all_but_small or exclude_large:
      for this_entity in d.get_entities_in_room(entity.pos):     # list the locations of visible entities in the room, excluding small size ones, and of course excluding the targeter and target
        if (not this_entity.no_longer_here()) and this_entity is not entity:
          if this_entity.pos<>entity.pos and this_entity.pos<>to_pos:
            if (this_entity.info().size <> SZ_SMALL and exclude_all_but_small) or (this_entity.info().size==SZ_LARGE and exclude_large):
              if entity.can_see_if_in_los(d,this_entity):
                exclude.append(this_entity.pos)    
    down = to_pos.down - entity.pos.down
    right = to_pos.right - entity.pos.right
    ray_options = self.rays.r[down][right]
    for ray in ray_options:
      line_of_shot = []
      for ray_tile in self.rays.seq[ray]:
        this_pos.down = entity.pos.down + ray_tile[0]
        this_pos.right = entity.pos.right + ray_tile[1]
        if this_pos.down==to_pos.down and this_pos.right==to_pos.right:
          line_of_shot.append(this_pos)
          return line_of_shot
        if this_room.tiles[this_pos.down][this_pos.right].info().shootable:
          if this_pos not in exclude:
            line_of_shot.append(this_pos)          
            continue
        break
    return None

  def nearby_unthreatened_space(self,entity):
    # return a walkable empty adjacent Pos with no enemies adjacent to it, or None
    for dir in permute(DIRS_AROUND):
      this_pos = get_pos_in_dir(self,entity.pos,dir)
      if self.l.valid(this_pos) and self.l.tile_at(this_pos).info().walkable(entity):
        occupant = self.get_occupant(this_pos)
        if occupant is None:
          ok = 1
          for dir2 in DIRS_AROUND:
            that_pos = get_pos_in_dir(self,this_pos,dir2)
            if self.l.valid(that_pos):
              occupant = self.get_occupant(that_pos)
              if occupant is not None and entity.is_enemy(occupant):
                ok = 0
                break
          if self.l.tile_at(this_pos).info().slow_without_webwalk or self.l.tile_at(this_pos).info().is_fiery or self.l.tile_at(this_pos).info().fall_into:
            ok = 0
          if ok:
            return this_pos
    return None
  
  def get_entities_in_room(self,pos):
    return self.entity_rooms.get((pos.level,pos.r_down,pos.r_right),[])
  
  def get_entities_in_nearby_room(self,pos):
    result = self.entity_rooms.get((pos.level,pos.r_down,pos.r_right),[])
    this_level = self.l.level_at(pos)
    for down_right in self.shells[1]:
      r_down = pos.r_down + down_right[0]
      r_right = pos.r_right + down_right[1]
      if r_down < this_level.r_height and r_down>=0 and r_right < this_level.r_width and r_right>=0:
        result = result + self.entity_rooms.get((pos.level,r_down,r_right),[])
    return result
  
  def update_entity_rooms(self,entity,old_pos,new_pos):
    # either old_pos or new_pos can be None
    if old_pos is not None:
      old_tuple = (old_pos.level,old_pos.r_down,old_pos.r_right)
    else:
      old_tuple = None
    if new_pos is not None:
      new_tuple = (new_pos.level,new_pos.r_down,new_pos.r_right)
    else:
      new_tuple = None
    if old_tuple is not None and new_tuple is not None and old_tuple[0]==new_tuple[0] and old_tuple[1]==new_tuple[1] and old_tuple[2]==new_tuple[2]:
      return # nothing needs to be done
    if old_tuple is not None and (old_tuple in self.entity_rooms):
      if entity in self.entity_rooms.get(old_tuple,[]):
        self.entity_rooms[old_tuple].remove(entity)
    if new_tuple is not None:
      if not (new_tuple in self.entity_rooms):
        self.entity_rooms[new_tuple] = []
      self.entity_rooms[new_tuple].append(entity)
    
  def add_keys(self):
    # Go through and drop keys.
    # Really part of the dungeon creation, but gets delayed to here cos the pathfinder is needed.
    keys_to_drop = [] # [(O_ code,(Pos to not pass through))]
    key_dict = {'G':O_KEY_GREEN,'R':O_KEY_RED,'B':O_KEY_BLUE,'$':O_KEY_GOLD,'S':O_KEY_SILVER,'\\':O_KEY_GOLD,'`':O_KEY_SILVER,'-':O_KEY_GREEN,'=':O_KEY_RED,'_':O_KEY_BLUE}
    for level_no in range(self.l.n_levels):
      this_level = self.l.levels[level_no]
      this_ready_level = self.l.ready_dungeon.levels[level_no]
      for r_i in range(this_level.r_height):
        for r_j in range(this_level.r_width):
          this_room = this_level.rooms[r_i][r_j]
          this_ready_room = this_ready_level.rooms[r_i][r_j]
          for key_type in this_ready_room.keys_required_somewhere:
            all_exits_of_room = ((level_no,r_i,r_j,DIR_UP),(level_no,r_i,r_j,DIR_DOWN),(level_no,r_i,r_j,DIR_LEFT),(level_no,r_i,r_j,DIR_RIGHT))
            keys_to_drop.append((key_type,all_exits_of_room))
          for key_type_and_pos in this_ready_room.gate_keys_required:
            keys_to_drop.append((key_type_and_pos[0],[]))
          for key_type_and_dir in this_ready_room.door_keys_required:
            keys_to_drop.append((key_type_and_dir[0],[(level_no,r_i,r_j,key_type_and_dir[1])]))
    # print keys_to_drop
    key_nums_dropped = []
    pos_to_start_from = self.l.entry_point
    while len(key_nums_dropped) < len(keys_to_drop):
      key_num_to_try = random.randint(0,len(keys_to_drop)-1)
      if not key_num_to_try in key_nums_dropped:      
        if not (keys_to_drop[key_num_to_try][0] in key_dict.keys()):
          key_nums_dropped.append(key_num_to_try)
          continue
        level_no = random.randint(0,self.l.n_levels-1)
        this_level = self.l.levels[level_no]
        r_i = random.randint(0,this_level.r_height-1)
        r_j = random.randint(0,this_level.r_width-1)
        pos = Pos(level_no,r_i,r_j,0,0)
        this_room = self.l.room_at(pos)
        i = random.randint(0,this_room.height-1)
        j = random.randint(0,this_room.width-1)
        pos.down = i
        pos.right = j
        this_tile = self.l.tile_at(pos)
        if this_tile.type==T_FLOOR:
          if this_tile.object in (O_COPPER,O_SILVER,O_GOLD,O_GEMS,O_CHEST_SMALL) or (this_tile.object is None and random.random()<0.1):
            posses_to_avoid = []
            for k in range(len(keys_to_drop)):
              if not (k in key_nums_dropped):
                for pos_to_avoid in keys_to_drop[k][1]:
                  posses_to_avoid.append(pos_to_avoid)
                  if pos_to_avoid[3]==DIR_UP:
                    posses_to_avoid.append((pos_to_avoid[0],pos_to_avoid[1]-1,pos_to_avoid[2],DIR_DOWN))
                  elif pos_to_avoid[3]==DIR_DOWN:
                    posses_to_avoid.append((pos_to_avoid[0],pos_to_avoid[1]+1,pos_to_avoid[2],DIR_UP))
                  elif pos_to_avoid[3]==DIR_RIGHT:
                    posses_to_avoid.append((pos_to_avoid[0],pos_to_avoid[1],pos_to_avoid[2]+1,DIR_LEFT))
                  elif pos_to_avoid[3]==DIR_LEFT:
                    posses_to_avoid.append((pos_to_avoid[0],pos_to_avoid[1],pos_to_avoid[2]-1,DIR_RIGHT))
            # test condition follows- can go from 'pos_to_start_from' to 'pos' without going through any of 'posses_to_avoid'
            # print "considering whether we can go from ",pos_to_start_from," to ",pos," without going through: "
            # for pos_to_avoid in posses_to_avoid:
              # print pos_to_avoid
            if path_available_for_key_placement(self,pos_to_start_from,pos,posses_to_avoid):
              # print "putting a key to door of type ",keys_to_drop[key_num_to_try][0]," at ",pos
              self.l.tile_at(pos).object = Object(key_dict[keys_to_drop[key_num_to_try][0]])
              key_nums_dropped.append(key_num_to_try)
            else:
              pass
              # print "considered but rejected putting a key to door of type ",keys_to_drop[key_num_to_try][0]," at ",pos
    # Ah, but we also need to drop keys associated with levels
    for level_no in range(self.l.n_levels):
      this_level = self.l.levels[level_no]
      this_ready_level = self.l.ready_dungeon.levels[level_no]
      keys_required_somewhere = this_ready_level.keys_required_somewhere
      for key in keys_required_somewhere:
        while 1:
          this_level = self.l.levels[level_no]
          r_i = random.randint(0,this_level.r_height-1)
          r_j = random.randint(0,this_level.r_width-1)
          pos = Pos(level_no,r_i,r_j,0,0)
          this_room = self.l.room_at(pos)
          i = random.randint(0,this_room.height-1)
          j = random.randint(0,this_room.width-1)
          pos.down = i
          pos.right = j
          this_tile = self.l.tile_at(pos)
          if this_tile.type==T_FLOOR:
            if this_tile.object in (O_COPPER,O_SILVER,O_GOLD,O_GEMS,O_CHEST_SMALL) or (this_tile.object is None and random.random()<0.1):
              self.l.tile_at(pos).object = Object(key_dict[key])
              print "placing a level-associated key of type ",key_dict[key]," at ",pos
              break
    del self.l.ready_dungeon
    del self.room_network_for_keys
    
  def get_final_message(self):
    character = self.get_character()
    message = []
    message.append("HP: " + str(int(floor(character.hp))) + "/" + str(int(floor(character.original_hp))))
    message.append("MP: " + str(int(floor(character.mp))) + "/" + str(int(floor(character.original_mp))))
    message.append("\nPowers learnt:")
    if len(character.learned_powers)==0:
      message.append("None")
    else:
      for p_code in character.learned_powers:
        message.append("  "+power_names[p_code])
    message.append("")
    message.append("Equipment:")
    objects = [character.garb,character.melee_weapon,character.missile_weapon]+character.items+character.keys
    for object in objects:
      if object is not None:
        message.append(olib.o[object.type].name)
    message.append("")
    message.append("Speed: " + str(int(floor(character.stats.speed))))
    message.append("Magic Res: " + str(int(floor(character.stats.magic_resistance))))
    message.append("Armor: " + str(int(floor(character.stats.armor))))
    message.append("Stealth: " + str(int(floor(character.stats.stealth))))
    message.append("")    
    if len(self.special_levels_already_visited)>1:
      message.append("Special areas visited:")       
      for d_code in self.special_levels_already_visited:
        message.append(dlib.d[d_code].long_name)
    message.append("")
    message.append("He can see:")
    anything = 0
    for entity in self.get_entities_in_room(character.pos):
      if not entity.is_character:
        message.append(entity.name)
        anything = 1
    # print character.pos
    room = self.l.room_at(character.pos)
    for i in range(room.height):
      for j in range(room.width):
        object = room.tiles[i][j].object
        if object is not None:
          message.append(object.info().name)
          anything = 1
    if not anything:
      message.append("nothing unusual.")
    message.append("")
    message.append("The last messages he received were:\n")
    messages_to_play = display.last_messages[max(0,len(display.last_messages)-10):len(display.last_messages)]
    for message_to_play in messages_to_play:
      message = message + message_to_play
      message.append("")
    return message

  def try_drop(self,display,map_pos,pos_to_drop_in,slot,number=0,try_scatter=1):
    if map_pos is None:
        return 0
    character = self.get_character()
    if self.l.tile_at(pos_to_drop_in).object is None and self.l.tile_at(pos_to_drop_in).info().can_contain_objects:
      if slot==SL_GARB:
        object_to_drop = character.garb
      elif slot==SL_MELEE:
        object_to_drop = character.melee_weapon
      elif slot==SL_MISSILE:
        object_to_drop = character.missile_weapon
      elif slot==SL_KEY:
        object_to_drop = character.keys[number]
      else:
        object_to_drop = character.items[number]
      self.l.tile_at(pos_to_drop_in).object = object_to_drop
      display.send_message("You drop: "+object_to_drop.info().name)
      sound.play("sword1")
      if slot==SL_GARB:
        character.garb = None
      elif slot==SL_MELEE:
        character.melee_weapon = None
      elif slot==SL_MISSILE:
        character.missile_weapon = None
      elif slot==SL_KEY:
        character.keys[number] = None
      else:
        character.items[number] = None
      self.get_character().info().recalc(self)
      occupant = self.get_occupant(pos_to_drop_in)
      display.redraw_tile(self,pos_to_drop_in,occupant)  
      return 1    
    elif try_scatter:
      for dir in [DIR_RIGHT,DIR_NE,DIR_NW,DIR_UP,DIR_DOWN,DIR_SE,DIR_SW,DIR_LEFT]:
          this_pos = get_pos_in_dir(self,pos_to_drop_in,dir)
          if self.try_drop(display,this_pos,this_pos,slot,number=number,try_scatter=0)==1:
              return 1
      display.send_message("You cannot drop something here. Stand on an empty space and then try again.")
      sound.play("cancel1")
      return 1
    return 0
  
def new_dungeon(old_d,new_dungeon_code,new_difficulty):
  # The character has left through an exit.
  # A new dungeon is being created.
  # We will populate it and return the d object.
  # Some things stay the same.
  old_character = old_d.get_character()
  d = Dungeon(new_dungeon_code,new_difficulty,
              old_character.level,
              old_d.most_recent_main_dungeon_entry_difficulty,
              old_d.special_levels_already_visited) # new_difficulty is only used if new_dungeon_code is D_MAIN_DUNGEON
  d.turn = old_d.turn
  new_character = d.get_character()
  new_character.score = old_character.score
  new_character.level = old_character.level
  new_character.learned_powers = old_character.learned_powers
  new_character.forgotten_powers = old_character.forgotten_powers
  new_character.powers_used = old_character.powers_used
  new_character.hp = old_character.hp
  new_character.original_hp = old_character.original_hp
  new_character.mp = old_character.mp
  new_character.original_mp = old_character.original_mp
  new_character.zap = old_character.zap  
  new_character.energy = old_character.energy
  new_character.heal_max_hp = old_character.heal_max_hp 
  new_character.turn_at_last_after_action_update = old_character.turn_at_last_after_action_update
  if old_character.melee_weapon is not None:
    new_character.melee_weapon = Object(old_character.melee_weapon.type)
  if old_character.missile_weapon is not None:
    new_character.missile_weapon = Object(old_character.missile_weapon.type)
  if old_character.garb is not None:
    new_character.garb = Object(old_character.garb.type)
  for i in range(0,6):
    if old_character.items[i] is not None: 
      new_character.items[i] = Object(old_character.items[i].type)
  # keys disappear
  new_character.info().recalc(d,quiet=1)
  return d


d = None  # fill this in with a Dungeon() in __main__.py

# do some psyco bindings 
psyco.bind(Dungeon.get_active_pos)
psyco.bind(Dungeon.get_character)
